entity stepA_mal is
end entity stepA_mal;

library STD;
use STD.textio.all;
library WORK;
use WORK.pkg_readline.all;
use WORK.types.all;
use WORK.printer.all;
use WORK.reader.all;
use WORK.env.all;
use WORK.core.all;

architecture test of stepA_mal is

  shared variable repl_env: env_ptr;

  procedure mal_READ(str: in string; ast: out mal_val_ptr; err: out mal_val_ptr) is
  begin
    read_str(str, ast, err);
  end procedure mal_READ;

  procedure starts_with(lst : inout mal_val_ptr;
                        sym : in    string;
                        res :   out boolean) is
  begin
    res := lst.seq_val.all'length = 2
       and lst.seq_val.all (lst.seq_val.all'low).val_type = mal_symbol
       and lst.seq_val.all (lst.seq_val.all'low).string_val.all = sym;
  end starts_with;

  -- Forward declaration
  procedure quasiquote(ast:    inout mal_val_ptr;
                       result:   out mal_val_ptr);

  procedure qq_loop(elt : inout mal_val_ptr;
                    acc : inout mal_val_ptr) is
    variable sw  : boolean     := elt.val_type = mal_list;
    variable seq : mal_seq_ptr := new mal_seq(0 to 2);
  begin
    if sw then
      starts_with(elt, "splice-unquote", sw);
    end if;
    if sw then
      new_symbol("concat", seq(0));
      seq(1) := elt.seq_val(1);
    else
      new_symbol("cons", seq(0));
      quasiquote(elt, seq(1));
    end if;
    seq(2) := acc;
    new_seq_obj(mal_list, seq, acc);
  end qq_loop;

  procedure qq_foldr (xs  : inout mal_seq_ptr;
                      res :   out mal_val_ptr) is
    variable seq : mal_seq_ptr := new mal_seq(0 to -1);
    variable acc : mal_val_ptr;
  begin
    new_seq_obj(mal_list, seq, acc);
    for i in xs'reverse_range loop
      qq_loop (xs(i), acc);
    end loop;
    res := acc;
  end procedure qq_foldr;

  procedure quasiquote(ast:    inout mal_val_ptr;
                       result:   out mal_val_ptr) is
    variable sw  : boolean;
    variable seq : mal_seq_ptr;
  begin
    case ast.val_type is
      when mal_list =>
        starts_with(ast, "unquote", sw);
        if sw then
          result := ast.seq_val(1);
        else
          qq_foldr(ast.seq_val, result);
        end if;
      when mal_vector =>
        seq := new mal_seq(0 to 1);
        new_symbol("vec", seq(0));
        qq_foldr(ast.seq_val, seq(1));
        new_seq_obj(mal_list, seq, result);
      when mal_symbol | mal_hashmap =>
        seq := new mal_seq(0 to 1);
        new_symbol("quote", seq(0));
        seq(1) := ast;
        new_seq_obj(mal_list, seq, result);
      when others =>
        result := ast;
    end case;
  end procedure quasiquote;

  -- Forward declaration
  procedure EVAL(in_ast: inout mal_val_ptr; in_env: inout env_ptr; result: out mal_val_ptr; err: out mal_val_ptr);

  procedure apply_func(fn: inout mal_val_ptr; args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr);

  procedure fn_eval(args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
  begin
    EVAL(args.seq_val(0), repl_env, result, err);
  end procedure fn_eval;

  procedure fn_swap(args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
    variable atom: mal_val_ptr := args.seq_val(0);
    variable fn: mal_val_ptr := args.seq_val(1);
    variable call_args_seq: mal_seq_ptr;
    variable call_args, eval_res, sub_err: mal_val_ptr;
  begin
    call_args_seq := new mal_seq(0 to args.seq_val'length - 2);
    call_args_seq(0) := atom.seq_val(0);
    call_args_seq(1 to call_args_seq'length - 1) := args.seq_val(2 to args.seq_val'length - 1);
    new_seq_obj(mal_list, call_args_seq, call_args);
    apply_func(fn, call_args, eval_res, sub_err);
    if sub_err /= null then
      err := sub_err;
      return;
    end if;
    atom.seq_val(0) := eval_res;
    result := eval_res;
  end procedure fn_swap;

  procedure fn_apply(args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
    variable fn: mal_val_ptr := args.seq_val(0);
    variable rest: mal_val_ptr;
    variable mid_args_count, rest_args_count: integer;
    variable call_args: mal_val_ptr;
    variable call_args_seq: mal_seq_ptr;
  begin
    rest := args.seq_val(args.seq_val'high);
    mid_args_count := args.seq_val'length - 2;
    rest_args_count := rest.seq_val'length;
    call_args_seq := new mal_seq(0 to mid_args_count + rest_args_count - 1);
    call_args_seq(0 to mid_args_count - 1) := args.seq_val(1 to args.seq_val'length - 2);
    call_args_seq(mid_args_count to call_args_seq'high) := rest.seq_val(rest.seq_val'range);
    new_seq_obj(mal_list, call_args_seq, call_args);
    apply_func(fn, call_args, result, err);
  end procedure fn_apply;

  procedure fn_map(args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
    variable fn: mal_val_ptr := args.seq_val(0);
    variable lst: mal_val_ptr := args.seq_val(1);
    variable call_args, sub_err: mal_val_ptr;
    variable new_seq: mal_seq_ptr;
    variable i: integer;
  begin
    new_seq := new mal_seq(lst.seq_val'range);  -- (0 to lst.seq_val.length - 1);
    for i in new_seq'range loop
      new_one_element_list(lst.seq_val(i), call_args);
      apply_func(fn, call_args, new_seq(i), sub_err);
      if sub_err /= null then
        err := sub_err;
        return;
      end if;
    end loop;
    new_seq_obj(mal_list, new_seq, result);
  end procedure fn_map;

  procedure apply_native_func(func_sym: inout mal_val_ptr; args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
  begin
    if func_sym.string_val.all = "eval" then
      fn_eval(args, result, err);
    elsif func_sym.string_val.all = "swap!" then
      fn_swap(args, result, err);
    elsif func_sym.string_val.all = "apply" then
      fn_apply(args, result, err);
    elsif func_sym.string_val.all = "map" then
      fn_map(args, result, err);
    else
      eval_native_func(func_sym, args, result, err);
    end if;
  end procedure apply_native_func;

  procedure apply_func(fn: inout mal_val_ptr; args: inout mal_val_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
    variable fn_env: env_ptr;
  begin
    case fn.val_type is
      when mal_nativefn =>
        apply_native_func(fn, args, result, err);
      when mal_fn =>
        new_env(fn_env, fn.func_val.f_env, fn.func_val.f_args, args);
        EVAL(fn.func_val.f_body, fn_env, result, err);
      when others =>
        new_string("not a function", err);
        return;
    end case;
  end procedure apply_func;

  procedure eval_ast_seq(ast_seq : inout mal_seq_ptr;
                         skip    : in    natural;
                         env     : inout env_ptr;
                         result  : inout mal_seq_ptr;
                         err     :   out mal_val_ptr) is
    variable eval_err: mal_val_ptr;
  begin
    result := new mal_seq(0 to ast_seq'length - 1 - skip);
    for i in result'range loop
      EVAL(ast_seq(skip + i), env, result(i), eval_err);
      if eval_err /= null then
        err := eval_err;
        return;
      end if;
    end loop;
  end procedure eval_ast_seq;

  procedure EVAL(in_ast : inout mal_val_ptr;
                 in_env : inout env_ptr;
                 result :   out mal_val_ptr;
                 err    :   out mal_val_ptr) is
    variable val, eval_err, a0, call_args, vars, fn, sub_err: mal_val_ptr;
    variable ast : mal_val_ptr := in_ast;
    variable env : env_ptr     := in_env;
    variable let_env, catch_env, fn_env : env_ptr;
    variable s: line;
    variable new_seq: mal_seq_ptr;
    variable i: integer;
  begin
    loop

    new_symbol("DEBUG-EVAL", a0);
    env_get(env, a0, val);
    if val /= null and val.val_type /= mal_nil and val.val_type /= mal_false
    then
      mal_printstr("EVAL: ");
      pr_str(ast, true, s);
      mal_printline(s.all);
    end if;

    case ast.val_type is
      when mal_symbol =>
        env_get(env, ast, val);
        if val = null then
          new_string("'" & ast.string_val.all & "' not found", err);
          return;
        end if;
        result := val;
        return;
      when mal_list =>
        null;
      when mal_vector | mal_hashmap =>
        eval_ast_seq(ast.seq_val, 0, env, new_seq, eval_err);
        if eval_err /= null then
          err := eval_err;
          return;
        end if;
        new_seq_obj(ast.val_type, new_seq, result);
        return;
      when others =>
        result := ast;
        return;
    end case;

      if ast.seq_val'length = 0 then
        result := ast;
        return;
      end if;

      a0 := ast.seq_val(0);
      if a0.val_type = mal_symbol then
        if a0.string_val.all = "def!" then
          EVAL(ast.seq_val(2), env, val, sub_err);
          if sub_err /= null then
            err := sub_err;
            return;
          end if;
          env_set(env, ast.seq_val(1), val);
          result := val;
          return;

        elsif a0.string_val.all = "let*" then
          vars := ast.seq_val(1);
          new_env(let_env, env);
          i := 0;
          while i < vars.seq_val'length loop
            EVAL(vars.seq_val(i + 1), let_env, val, sub_err);
            if sub_err /= null then
              err := sub_err;
              return;
            end if;
            env_set(let_env, vars.seq_val(i), val);
            i := i + 2;
          end loop;
          env := let_env;
          ast := ast.seq_val(2);
          next; -- TCO

        elsif a0.string_val.all = "quote" then
          result := ast.seq_val(1);
          return;

        elsif a0.string_val.all = "quasiquote" then
          quasiquote(ast.seq_val(1), ast);
          next; -- TCO

        elsif a0.string_val.all = "defmacro!" then
          EVAL(ast.seq_val(2), env, fn, sub_err);
          if sub_err /= null then
            err := sub_err;
            return;
          end if;
          new_fn(fn.func_val.f_body, fn.func_val.f_args, fn.func_val.f_env, val);
          val.func_val.f_is_macro := true;
          env_set(env, ast.seq_val(1), val);
          result := val;
          return;

        elsif a0.string_val.all = "try*" then
          EVAL(ast.seq_val(1), env, result, sub_err);
          if sub_err /= null then
            if ast.seq_val'length > 2 and
               ast.seq_val(2).val_type = mal_list and
               ast.seq_val(2).seq_val(0).val_type = mal_symbol and
               ast.seq_val(2).seq_val(0).string_val.all = "catch*" then
              new_one_element_list(ast.seq_val(2).seq_val(1), vars);
              new_one_element_list(sub_err, call_args);
              new_env(catch_env, env, vars, call_args);
              EVAL(ast.seq_val(2).seq_val(2), catch_env, result, err);
            else
              err := sub_err;
              return;
            end if;
          end if;
          return;

        elsif a0.string_val.all = "do" then
          for i in 1 to ast.seq_val'high - 1 loop
            EVAL(ast.seq_val(i), env, result, sub_err);
            if sub_err /= null then
              err := sub_err;
              return;
            end if;
          end loop;
          ast := ast.seq_val(ast.seq_val'high);
          next; -- TCO

        elsif a0.string_val.all = "if" then
          EVAL(ast.seq_val(1), env, val, sub_err);
          if sub_err /= null then
            err := sub_err;
            return;
          end if;
          if val.val_type = mal_nil or val.val_type = mal_false then
            if ast.seq_val'length > 3 then
              ast := ast.seq_val(3);
            else
              new_nil(result);
              return;
            end if;
          else
            ast := ast.seq_val(2);
          end if;
          next; -- TCO

        elsif a0.string_val.all = "fn*" then
          new_fn(ast.seq_val(2), ast.seq_val(1), env, result);
          return;

        end if;
      end if;

      EVAL (a0, env, fn, sub_err);
      if sub_err /= null then
        err := sub_err;
        return;
      end if;
      -- Special-case macros
      if fn.val_type = mal_fn and fn.func_val.f_is_macro then
        seq_drop_prefix(ast, 1, call_args);
        apply_func(fn, call_args, ast, sub_err);
        if sub_err /= null then
          err := sub_err;
          return;
        end if;
        next; -- TCO
      end if;
      -- Evaluate arguments
      eval_ast_seq(ast.seq_val, 1, env, new_seq, sub_err);
      if sub_err /= null then
        err := sub_err;
        return;
      end if;
      new_seq_obj(mal_list, new_seq, call_args);
      -- Special-case functions for TCO
      if fn.val_type = mal_fn then
          new_env(fn_env, fn.func_val.f_env, fn.func_val.f_args, call_args);
          env := fn_env;
          ast := fn.func_val.f_body;
          next; -- TCO
      end if;
      apply_func(fn, call_args, result, err);
      return;
    end loop;
  end procedure EVAL;

  procedure mal_PRINT(exp: inout mal_val_ptr; result: out line) is
  begin
    pr_str(exp, true, result);
  end procedure mal_PRINT;

  procedure RE(str: in string; env: inout env_ptr; result: out mal_val_ptr; err: out mal_val_ptr) is
    variable ast, read_err: mal_val_ptr;
  begin
    mal_READ(str, ast, read_err);
    if read_err /= null then
      err := read_err;
      result := null;
      return;
    end if;
    if ast = null then
      result := null;
      return;
    end if;
    EVAL(ast, env, result, err);
  end procedure RE;

  procedure REP(str: in string; env: inout env_ptr; result: out line; err: out mal_val_ptr) is
    variable eval_res, eval_err: mal_val_ptr;
  begin
    RE(str, env, eval_res, eval_err);
    if eval_err /= null then
      err := eval_err;
      result := null;
      return;
    end if;
    mal_PRINT(eval_res, result);
  end procedure REP;

  procedure set_argv(e: inout env_ptr; program_file: inout line) is
    variable argv_var_name: string(1 to 6) := "*ARGV*";
    variable argv_sym, argv_list: mal_val_ptr;
    file f: text;
    variable status: file_open_status;
    variable one_line: line;
    variable seq: mal_seq_ptr;
    variable element: mal_val_ptr;
  begin
    program_file := null;
    seq := new mal_seq(0 to -1);
    file_open(status, f, external_name => "vhdl_argv.tmp", open_kind => read_mode);
    if status = open_ok then
      if not endfile(f) then
        readline(f, program_file);
        while not endfile(f) loop
          readline(f, one_line);
          new_string(one_line.all, element);
          seq := new mal_seq'(seq.all & element);
        end loop;
      end if;
      file_close(f);
    end if;
    new_seq_obj(mal_list, seq, argv_list);
    new_symbol(argv_var_name, argv_sym);
    env_set(e, argv_sym, argv_list);
  end procedure set_argv;

  procedure repl is
    variable is_eof: boolean;
    variable program_file, input_line, result: line;
    variable eval_sym, eval_fn, dummy_val, err: mal_val_ptr;
    variable outer: env_ptr;
    variable eval_func_name: string(1 to 4) := "eval";
  begin
    outer := null;
    new_env(repl_env, outer);

    -- core.EXT: defined using VHDL (see core.vhdl)
    define_core_functions(repl_env);
    new_symbol(eval_func_name, eval_sym);
    new_nativefn(eval_func_name, eval_fn);
    env_set(repl_env, eval_sym, eval_fn);
    set_argv(repl_env, program_file);

    -- core.mal: defined using the language itself
    RE("(def! *host-language* " & '"' & "vhdl" & '"' & ")", repl_env, dummy_val, err);
    RE("(def! not (fn* (a) (if a false true)))", repl_env, dummy_val, err);
    RE("(def! load-file (fn* (f) (eval (read-string (str " & '"' & "(do " & '"' & " (slurp f) " & '"' & "\nnil)" & '"' & ")))))", repl_env, dummy_val, err);
    RE("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw " & '"' & "odd number of forms to cond" & '"' & ")) (cons 'cond (rest (rest xs)))))))", repl_env, dummy_val, err);

    if program_file /= null then
      REP("(load-file " & '"' & program_file.all & '"' & ")", repl_env, result, err);
      return;
    end if;

    RE("(println (str " & '"' & "Mal [" & '"' & " *host-language* " & '"' & "]" & '"' & "))", repl_env, dummy_val, err);
    loop
      mal_readline("user> ", is_eof, input_line);
      exit when is_eof;
      next when input_line'length = 0;
      REP(input_line.all, repl_env, result, err);
      if err /= null then
        pr_str(err, false, result);
        result := new string'("Error: " & result.all);
      end if;
      if result /= null then
        mal_printline(result.all);
      end if;
      deallocate(result);
      deallocate(err);
    end loop;
    mal_printline("");
  end procedure repl;

begin
  repl;
end architecture test;
